package com.airbnb.epoxy;

import static com.airbnb.epoxy.Utils.getElementByName;
import static com.airbnb.epoxy.Utils.getEpoxyObjectType;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeVariableName;

import java.util.List;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

class BasicGeneratedModelInfo extends GeneratedModelInfo {

 private final Elements elementUtils;

 BasicGeneratedModelInfo(Types typeUtils, Elements elementUtils, TypeElement superClassElement,
     ErrorLogger errorLogger) {
   this.elementUtils = elementUtils;
   this.superClassName = ParameterizedTypeName.get(superClassElement.asType());
   this.superClassElement = superClassElement;
   generatedClassName = buildGeneratedModelName(superClassElement);

   for (TypeParameterElement typeParameterElement : superClassElement.getTypeParameters()) {
     typeVariableNames.add(TypeVariableName.get(typeParameterElement));
   }

   collectOriginalClassConstructors(superClassElement);
   collectMethodsReturningClassType(superClassElement, typeUtils);

   if (!typeVariableNames.isEmpty()) {
     TypeVariableName[] typeArguments =
         typeVariableNames.toArray(new TypeVariableName[typeVariableNames.size()]);
     this.parametrizedClassName = ParameterizedTypeName.get(generatedClassName, typeArguments);
   } else {
     this.parametrizedClassName = generatedClassName;
   }

   TypeMirror boundObjectTypeMirror = getEpoxyObjectType(superClassElement, typeUtils);
   if (boundObjectTypeMirror == null) {
     errorLogger
         .logError("Epoxy model type could not be found. (class: %s)",
             superClassElement.getSimpleName());
     // Return a basic view type so the code can be generated
     boundObjectTypeMirror =
         getElementByName(Utils.HARMONY_COMPONENT_TYPE, elementUtils, typeUtils).asType();
   }
   boundObjectTypeName = TypeName.get(boundObjectTypeMirror);

   EpoxyModelClass annotation = superClassElement.getAnnotation(EpoxyModelClass.class);
   boolean hasEpoxyClassAnnotation = annotation != null;
   boolean isAbstract = superClassElement.getModifiers().contains(Modifier.ABSTRACT);

   // By default we don't extend classes that are abstract; if they don't contain all required
   // methods then our generated class won't compile. If there is a EpoxyModelClass annotation
   // though we will always generate the subclass
   shouldGenerateModel = !isAbstract || hasEpoxyClassAnnotation;
   includeOtherLayoutOptions = hasEpoxyClassAnnotation && annotation.useLayoutOverloads();
 }

 protected ClassName buildGeneratedModelName(TypeElement classElement) {
   String packageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();

   int packageLen = packageName.length() + 1;
   String className =
       classElement.getQualifiedName().toString().substring(packageLen).replace('.', '$');

   return ClassName.get(packageName, className + GENERATED_CLASS_NAME_SUFFIX);
 }

 /**
  * Get information about constructors of the original class so we can duplicate them in the
  * generated class and call through to super with the proper parameters
  */
 private void collectOriginalClassConstructors(TypeElement originalClass) {
   for (Element subElement : originalClass.getEnclosedElements()) {
     if (subElement.getKind() == ElementKind.CONSTRUCTOR
         && !subElement.getModifiers().contains(Modifier.PRIVATE)) {
       ExecutableElement castedSubElement = ((ExecutableElement) subElement);
       List<? extends VariableElement> params = castedSubElement.getParameters();
       constructors
           .add(new ConstructorInfo(subElement.getModifiers(), buildParamSpecs(params),
               castedSubElement.isVarArgs()));
     }
   }
 }
}
